/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.util.jar;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Collection;
import java.util.AbstractSet;
import java.util.Iterator;
import sun.util.logging.PlatformLogger;
import java.util.Comparator;
import sun.misc.ASCIICaseInsensitiveComparator;

/**
 * The Attributes class maps Manifest attribute names to associated string
 * values. Valid attribute names are case-insensitive, are restricted to
 * the ASCII characters in the set [0-9a-zA-Z_-], and cannot exceed 70
 * characters in length. Attribute values can contain any characters and
 * will be UTF8-encoded when written to the output stream.  See the
 * <a href="../../../../technotes/guides/jar/jar.html">JAR File Specification</a>
 * for more information about valid attribute names and values.
 *
 * @author David Connelly
 * @see Manifest
 * @since 1.2
 */
public class Attributes implements Map<Object, Object>, Cloneable {

  /**
   * The attribute name-value mappings.
   */
  protected Map<Object, Object> map;

  /**
   * Constructs a new, empty Attributes object with default size.
   */
  public Attributes() {
    this(11);
  }

  /**
   * Constructs a new, empty Attributes object with the specified
   * initial size.
   *
   * @param size the initial number of attributes
   */
  public Attributes(int size) {
    map = new HashMap<>(size);
  }

  /**
   * Constructs a new Attributes object with the same attribute name-value
   * mappings as in the specified Attributes.
   *
   * @param attr the specified Attributes
   */
  public Attributes(Attributes attr) {
    map = new HashMap<>(attr);
  }


  /**
   * Returns the value of the specified attribute name, or null if the
   * attribute name was not found.
   *
   * @param name the attribute name
   * @return the value of the specified attribute name, or null if not found.
   */
  public Object get(Object name) {
    return map.get(name);
  }

  /**
   * Returns the value of the specified attribute name, specified as
   * a string, or null if the attribute was not found. The attribute
   * name is case-insensitive.
   * <p>
   * This method is defined as:
   * <pre>
   *      return (String)get(new Attributes.Name((String)name));
   * </pre>
   *
   * @param name the attribute name as a string
   * @return the String value of the specified attribute name, or null if not found.
   * @throws IllegalArgumentException if the attribute name is invalid
   */
  public String getValue(String name) {
    return (String) get(new Attributes.Name(name));
  }

  /**
   * Returns the value of the specified Attributes.Name, or null if the
   * attribute was not found.
   * <p>
   * This method is defined as:
   * <pre>
   *     return (String)get(name);
   * </pre>
   *
   * @param name the Attributes.Name object
   * @return the String value of the specified Attribute.Name, or null if not found.
   */
  public String getValue(Name name) {
    return (String) get(name);
  }

  /**
   * Associates the specified value with the specified attribute name
   * (key) in this Map. If the Map previously contained a mapping for
   * the attribute name, the old value is replaced.
   *
   * @param name the attribute name
   * @param value the attribute value
   * @return the previous value of the attribute, or null if none
   * @throws ClassCastException if the name is not a Attributes.Name or the value is not a String
   */
  public Object put(Object name, Object value) {
    return map.put((Attributes.Name) name, (String) value);
  }

  /**
   * Associates the specified value with the specified attribute name,
   * specified as a String. The attributes name is case-insensitive.
   * If the Map previously contained a mapping for the attribute name,
   * the old value is replaced.
   * <p>
   * This method is defined as:
   * <pre>
   *      return (String)put(new Attributes.Name(name), value);
   * </pre>
   *
   * @param name the attribute name as a string
   * @param value the attribute value
   * @return the previous value of the attribute, or null if none
   * @throws IllegalArgumentException if the attribute name is invalid
   */
  public String putValue(String name, String value) {
    return (String) put(new Name(name), value);
  }

  /**
   * Removes the attribute with the specified name (key) from this Map.
   * Returns the previous attribute value, or null if none.
   *
   * @param name attribute name
   * @return the previous value of the attribute, or null if none
   */
  public Object remove(Object name) {
    return map.remove(name);
  }

  /**
   * Returns true if this Map maps one or more attribute names (keys)
   * to the specified value.
   *
   * @param value the attribute value
   * @return true if this Map maps one or more attribute names to the specified value
   */
  public boolean containsValue(Object value) {
    return map.containsValue(value);
  }

  /**
   * Returns true if this Map contains the specified attribute name (key).
   *
   * @param name the attribute name
   * @return true if this Map contains the specified attribute name
   */
  public boolean containsKey(Object name) {
    return map.containsKey(name);
  }

  /**
   * Copies all of the attribute name-value mappings from the specified
   * Attributes to this Map. Duplicate mappings will be replaced.
   *
   * @param attr the Attributes to be stored in this map
   * @throws ClassCastException if attr is not an Attributes
   */
  public void putAll(Map<?, ?> attr) {
    // ## javac bug?
    if (!Attributes.class.isInstance(attr)) {
      throw new ClassCastException();
    }
    for (Map.Entry<?, ?> me : (attr).entrySet()) {
      put(me.getKey(), me.getValue());
    }
  }

  /**
   * Removes all attributes from this Map.
   */
  public void clear() {
    map.clear();
  }

  /**
   * Returns the number of attributes in this Map.
   */
  public int size() {
    return map.size();
  }

  /**
   * Returns true if this Map contains no attributes.
   */
  public boolean isEmpty() {
    return map.isEmpty();
  }

  /**
   * Returns a Set view of the attribute names (keys) contained in this Map.
   */
  public Set<Object> keySet() {
    return map.keySet();
  }

  /**
   * Returns a Collection view of the attribute values contained in this Map.
   */
  public Collection<Object> values() {
    return map.values();
  }

  /**
   * Returns a Collection view of the attribute name-value mappings
   * contained in this Map.
   */
  public Set<Map.Entry<Object, Object>> entrySet() {
    return map.entrySet();
  }

  /**
   * Compares the specified Attributes object with this Map for equality.
   * Returns true if the given object is also an instance of Attributes
   * and the two Attributes objects represent the same mappings.
   *
   * @param o the Object to be compared
   * @return true if the specified Object is equal to this Map
   */
  public boolean equals(Object o) {
    return map.equals(o);
  }

  /**
   * Returns the hash code value for this Map.
   */
  public int hashCode() {
    return map.hashCode();
  }

  /**
   * Returns a copy of the Attributes, implemented as follows:
   * <pre>
   *     public Object clone() { return new Attributes(this); }
   * </pre>
   * Since the attribute names and values are themselves immutable,
   * the Attributes returned can be safely modified without affecting
   * the original.
   */
  public Object clone() {
    return new Attributes(this);
  }

  /*
   * Writes the current attributes to the specified data output stream.
   * XXX Need to handle UTF8 values and break up lines longer than 72 bytes
   */
  void write(DataOutputStream os) throws IOException {
    Iterator<Map.Entry<Object, Object>> it = entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry<Object, Object> e = it.next();
      StringBuffer buffer = new StringBuffer(
          ((Name) e.getKey()).toString());
      buffer.append(": ");

      String value = (String) e.getValue();
      if (value != null) {
        byte[] vb = value.getBytes("UTF8");
        value = new String(vb, 0, 0, vb.length);
      }
      buffer.append(value);

      buffer.append("\r\n");
      Manifest.make72Safe(buffer);
      os.writeBytes(buffer.toString());
    }
    os.writeBytes("\r\n");
  }

  /*
   * Writes the current attributes to the specified data output stream,
   * make sure to write out the MANIFEST_VERSION or SIGNATURE_VERSION
   * attributes first.
   *
   * XXX Need to handle UTF8 values and break up lines longer than 72 bytes
   */
  void writeMain(DataOutputStream out) throws IOException {
    // write out the *-Version header first, if it exists
    String vername = Name.MANIFEST_VERSION.toString();
    String version = getValue(vername);
    if (version == null) {
      vername = Name.SIGNATURE_VERSION.toString();
      version = getValue(vername);
    }

    if (version != null) {
      out.writeBytes(vername + ": " + version + "\r\n");
    }

    // write out all attributes except for the version
    // we wrote out earlier
    Iterator<Map.Entry<Object, Object>> it = entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry<Object, Object> e = it.next();
      String name = ((Name) e.getKey()).toString();
      if ((version != null) && !(name.equalsIgnoreCase(vername))) {

        StringBuffer buffer = new StringBuffer(name);
        buffer.append(": ");

        String value = (String) e.getValue();
        if (value != null) {
          byte[] vb = value.getBytes("UTF8");
          value = new String(vb, 0, 0, vb.length);
        }
        buffer.append(value);

        buffer.append("\r\n");
        Manifest.make72Safe(buffer);
        out.writeBytes(buffer.toString());
      }
    }
    out.writeBytes("\r\n");
  }

  /*
   * Reads attributes from the specified input stream.
   * XXX Need to handle UTF8 values.
   */
  void read(Manifest.FastInputStream is, byte[] lbuf) throws IOException {
    String name = null, value = null;
    byte[] lastline = null;

    int len;
    while ((len = is.readLine(lbuf)) != -1) {
      boolean lineContinued = false;
      if (lbuf[--len] != '\n') {
        throw new IOException("line too long");
      }
      if (len > 0 && lbuf[len - 1] == '\r') {
        --len;
      }
      if (len == 0) {
        break;
      }
      int i = 0;
      if (lbuf[0] == ' ') {
        // continuation of previous line
        if (name == null) {
          throw new IOException("misplaced continuation line");
        }
        lineContinued = true;
        byte[] buf = new byte[lastline.length + len - 1];
        System.arraycopy(lastline, 0, buf, 0, lastline.length);
        System.arraycopy(lbuf, 1, buf, lastline.length, len - 1);
        if (is.peek() == ' ') {
          lastline = buf;
          continue;
        }
        value = new String(buf, 0, buf.length, "UTF8");
        lastline = null;
      } else {
        while (lbuf[i++] != ':') {
          if (i >= len) {
            throw new IOException("invalid header field");
          }
        }
        if (lbuf[i++] != ' ') {
          throw new IOException("invalid header field");
        }
        name = new String(lbuf, 0, 0, i - 2);
        if (is.peek() == ' ') {
          lastline = new byte[len - i];
          System.arraycopy(lbuf, i, lastline, 0, len - i);
          continue;
        }
        value = new String(lbuf, i, len - i, "UTF8");
      }
      try {
        if ((putValue(name, value) != null) && (!lineContinued)) {
          PlatformLogger.getLogger("java.util.jar").warning(
              "Duplicate name in Manifest: " + name
                  + ".\n"
                  + "Ensure that the manifest does not "
                  + "have duplicate entries, and\n"
                  + "that blank lines separate "
                  + "individual sections in both your\n"
                  + "manifest and in the META-INF/MANIFEST.MF "
                  + "entry in the jar file.");
        }
      } catch (IllegalArgumentException e) {
        throw new IOException("invalid header field name: " + name);
      }
    }
  }

  /**
   * The Attributes.Name class represents an attribute name stored in
   * this Map. Valid attribute names are case-insensitive, are restricted
   * to the ASCII characters in the set [0-9a-zA-Z_-], and cannot exceed
   * 70 characters in length. Attribute values can contain any characters
   * and will be UTF8-encoded when written to the output stream.  See the
   * <a href="../../../../technotes/guides/jar/jar.html">JAR File Specification</a>
   * for more information about valid attribute names and values.
   */
  public static class Name {

    private String name;
    private int hashCode = -1;

    /**
     * Constructs a new attribute name using the given string name.
     *
     * @param name the attribute string name
     * @throws IllegalArgumentException if the attribute name was invalid
     * @throws NullPointerException if the attribute name was null
     */
    public Name(String name) {
      if (name == null) {
        throw new NullPointerException("name");
      }
      if (!isValid(name)) {
        throw new IllegalArgumentException(name);
      }
      this.name = name.intern();
    }

    private static boolean isValid(String name) {
      int len = name.length();
      if (len > 70 || len == 0) {
        return false;
      }
      for (int i = 0; i < len; i++) {
        if (!isValid(name.charAt(i))) {
          return false;
        }
      }
      return true;
    }

    private static boolean isValid(char c) {
      return isAlpha(c) || isDigit(c) || c == '_' || c == '-';
    }

    private static boolean isAlpha(char c) {
      return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
    }

    private static boolean isDigit(char c) {
      return c >= '0' && c <= '9';
    }

    /**
     * Compares this attribute name to another for equality.
     *
     * @param o the object to compare
     * @return true if this attribute name is equal to the specified attribute object
     */
    public boolean equals(Object o) {
      if (o instanceof Name) {
        Comparator<String> c = ASCIICaseInsensitiveComparator.CASE_INSENSITIVE_ORDER;
        return c.compare(name, ((Name) o).name) == 0;
      } else {
        return false;
      }
    }

    /**
     * Computes the hash value for this attribute name.
     */
    public int hashCode() {
      if (hashCode == -1) {
        hashCode = ASCIICaseInsensitiveComparator.lowerCaseHashCode(name);
      }
      return hashCode;
    }

    /**
     * Returns the attribute name as a String.
     */
    public String toString() {
      return name;
    }

    /**
     * <code>Name</code> object for <code>Manifest-Version</code>
     * manifest attribute. This attribute indicates the version number
     * of the manifest standard to which a JAR file's manifest conforms.
     *
     * @see <a href="../../../../technotes/guides/jar/jar.html#JAR_Manifest"> Manifest and Signature
     * Specification</a>
     */
    public static final Name MANIFEST_VERSION = new Name("Manifest-Version");

    /**
     * <code>Name</code> object for <code>Signature-Version</code>
     * manifest attribute used when signing JAR files.
     *
     * @see <a href="../../../../technotes/guides/jar/jar.html#JAR_Manifest"> Manifest and Signature
     * Specification</a>
     */
    public static final Name SIGNATURE_VERSION = new Name("Signature-Version");

    /**
     * <code>Name</code> object for <code>Content-Type</code>
     * manifest attribute.
     */
    public static final Name CONTENT_TYPE = new Name("Content-Type");

    /**
     * <code>Name</code> object for <code>Class-Path</code>
     * manifest attribute. Bundled extensions can use this attribute
     * to find other JAR files containing needed classes.
     *
     * @see <a href="../../../../technotes/guides/jar/jar.html#classpath"> JAR file
     * specification</a>
     */
    public static final Name CLASS_PATH = new Name("Class-Path");

    /**
     * <code>Name</code> object for <code>Main-Class</code> manifest
     * attribute used for launching applications packaged in JAR files.
     * The <code>Main-Class</code> attribute is used in conjunction
     * with the <code>-jar</code> command-line option of the
     * <tt>java</tt> application launcher.
     */
    public static final Name MAIN_CLASS = new Name("Main-Class");

    /**
     * <code>Name</code> object for <code>Sealed</code> manifest attribute
     * used for sealing.
     *
     * @see <a href="../../../../technotes/guides/jar/jar.html#sealing"> Package Sealing</a>
     */
    public static final Name SEALED = new Name("Sealed");

    /**
     * <code>Name</code> object for <code>Extension-List</code> manifest attribute
     * used for declaring dependencies on installed extensions.
     *
     * @see <a href="../../../../technotes/guides/extensions/spec.html#dependency"> Installed
     * extension dependency</a>
     */
    public static final Name EXTENSION_LIST = new Name("Extension-List");

    /**
     * <code>Name</code> object for <code>Extension-Name</code> manifest attribute
     * used for declaring dependencies on installed extensions.
     *
     * @see <a href="../../../../technotes/guides/extensions/spec.html#dependency"> Installed
     * extension dependency</a>
     */
    public static final Name EXTENSION_NAME = new Name("Extension-Name");

    /**
     * <code>Name</code> object for <code>Extension-Name</code> manifest attribute
     * used for declaring dependencies on installed extensions.
     *
     * @see <a href="../../../../technotes/guides/extensions/spec.html#dependency"> Installed
     * extension dependency</a>
     * @deprecated Extension mechanism will be removed in a future release. Use class path instead.
     */
    @Deprecated
    public static final Name EXTENSION_INSTALLATION = new Name("Extension-Installation");

    /**
     * <code>Name</code> object for <code>Implementation-Title</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779"> Java
     * Product Versioning Specification</a>
     */
    public static final Name IMPLEMENTATION_TITLE = new Name("Implementation-Title");

    /**
     * <code>Name</code> object for <code>Implementation-Version</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779"> Java
     * Product Versioning Specification</a>
     */
    public static final Name IMPLEMENTATION_VERSION = new Name("Implementation-Version");

    /**
     * <code>Name</code> object for <code>Implementation-Vendor</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779"> Java
     * Product Versioning Specification</a>
     */
    public static final Name IMPLEMENTATION_VENDOR = new Name("Implementation-Vendor");

    /**
     * <code>Name</code> object for <code>Implementation-Vendor-Id</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/extensions/versioning.html#applet"> Optional
     * Package Versioning</a>
     * @deprecated Extension mechanism will be removed in a future release. Use class path instead.
     */
    @Deprecated
    public static final Name IMPLEMENTATION_VENDOR_ID = new Name("Implementation-Vendor-Id");

    /**
     * <code>Name</code> object for <code>Implementation-URL</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/extensions/versioning.html#applet"> Optional
     * Package Versioning</a>
     * @deprecated Extension mechanism will be removed in a future release. Use class path instead.
     */
    @Deprecated
    public static final Name IMPLEMENTATION_URL = new Name("Implementation-URL");

    /**
     * <code>Name</code> object for <code>Specification-Title</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779"> Java
     * Product Versioning Specification</a>
     */
    public static final Name SPECIFICATION_TITLE = new Name("Specification-Title");

    /**
     * <code>Name</code> object for <code>Specification-Version</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779"> Java
     * Product Versioning Specification</a>
     */
    public static final Name SPECIFICATION_VERSION = new Name("Specification-Version");

    /**
     * <code>Name</code> object for <code>Specification-Vendor</code>
     * manifest attribute used for package versioning.
     *
     * @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779"> Java
     * Product Versioning Specification</a>
     */
    public static final Name SPECIFICATION_VENDOR = new Name("Specification-Vendor");
  }
}
